%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  Copyright by Wenliang Du                                        %%
%%  This work is licensed under the Creative Commons                %%
%%  Attribution-NonCommercial-ShareAlike 4.0 International License. %%
%%  To view a copy of this license, visit                           %%
%%  http://creativecommons.org/licenses/by-nc-sa/4.0/.              %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}



\newcommand{\miniVPN}{{\tt MiniVPN}\xspace}
\newcommand{\vpnFigs}{./Figs}

\lhead{\bfseries SEED Labs -- 隧道实验}


\newcommand{\hostu}{{\tt U}\xspace}
\newcommand{\hostv}{{\tt V}\xspace}


\begin{document}

\begin{center}
  {\LARGE VPN 隧道实验}
\end{center}

\seedlabcopyright{2020}




% *******************************************
% SECTION
% ******************************************* 
\section{概述}

虚拟专用网络（VPN）是建立在公共网络（通常是因特网）之上的专用网络。
VPN内的计算机可以安全地进行通信，就像它们在一个真实的、物理上与外界隔离的专用网络上，
即使它们的流量会通过公共网络。
VPN使得员工们在旅行时可以安全的接入公司网络；它使公司能够将他们的专用网络扩展到全国各地乃至世界各地。


本实验的目标是帮助学生理解VPN的工作原理。
我们专注于一种特定类型的，建立在传输层之上的，同时也是最常见的VPN。
我们将从头开始构建一个非常简单的VPN，并使用该过程来解释VPN技术的每个部分是如何工作的。
一个真正的VPN项目有两个基本部分，隧道和加密。本实验只关注隧道部分，帮助学生理解隧道技术，所以在这个实验中的隧道是没有被加密的。
另外还有一个更加全面且包括加密部分的VPN实验（VPN Lab）。
本实验涵盖以下主题：

\begin{itemize}[noitemsep]
  \item VPN(虚拟专用网络)
  \item TUN/TAP虚拟接口
  \item IP隧道
  \item 路由
\end{itemize}


\paragraph{书籍和视频}
TUN/TAP虚拟接口的详细介绍以及VPN的工作原理可参见以下内容：

\begin{itemize}[noitemsep]
  \item SEED Book 的第19章, \seedbook
  \item SEED Lecture 的第8部分, \seedisvideo
\end{itemize}


\paragraph{相关实验}
本实验仅涵盖VPN隧道部分，而完整的VPN还需要保护其隧道。
我们有一个另外单独的实验叫做VPN Lab，这是一个综合性的实验，包括隧道和保护部分。
学生可以先完成这个隧道实验。在学习了PKI和TLS之后，他们可以再做综合性的VPN实验。


\paragraph{实验环境} \seedenvironmentC

\newpage
% *******************************************
% SECTION
% ******************************************* 
\section{Task 1: 设置网络}

我们将在计算机（客户端）和网关之间建立一条VPN隧道，允许计算机安全地通过网关访问专用网络。
我们需要至少3台机器：VPN客户端（也用作主机 \hostu）、VPN服务器（路由器/网关）和一台在专用网络中的主机（主机\hostv）。
网络设置如图~\ref{vpn:fig:labsetup} 所示。


\begin{figure}[htb]
  \begin{center}
    \includegraphics[width=0.9\textwidth]{./Figs/VPN_2lans.pdf}
  \end{center}
  \caption{实验环境设置}
  \label{vpn:fig:labsetup}
\end{figure}


实际上，VPN客户端和VPN服务端是通过互联网连接的。
为了简单起见，我们在这个实验直接将这两台机器连接到同一个局域网，用这个局域网模拟互联网。


第三台机器，主机\hostv，是专用网络内的一台计算机。主机 \hostu 上的用户（专用网络之外）希望通过 VPN 隧道与主机 \hostv 通信。为了模拟这个设置，我们将主机 \hostv 连接到 VPN 服务器（也用作网关）。在这个设置里，主机 \hostv 不能直接从互联网访问，也不能从主机\hostu 直接访问。


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\paragraph{实验设置。}
\input{\commonfolder/container/setup}

\paragraph{共享文件夹。}
在这个实验里，我们需要写自己的代码，并在容器里运行。
\input{\commonfolder/container/volumes}

\paragraph{数据包嗅探。}
\input{\commonfolder/container/sniffing}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


\paragraph{测试}
请进行以下测试以确保实验环境设置正确：


\begin{itemize}[noitemsep]
  \item 主机 \hostu 可以与VPN Server通信。
  \item VPN Server可以与主机 \hostv 通信。
  \item 主机 \hostu 应该不能与主机 \hostv 通信。
  \item 在路由器上运行\texttt{tcpdump}，并嗅探每个网络上的流量以测试你是否可以捕获数据包。
\end{itemize}


% *******************************************
% SECTION
% *******************************************
\section{Task 2: 创建和配置TUN接口}

我们将要构建的VPN隧道基于TUN/TAP技术。
TUN和TAP是虚拟网络内核驱动程序，
他们实现了完全由软件支持的网络设备。
TAP模拟以太网设备，它针对的是网络第2层数据包（例如以太网帧），
TUN模拟网络层设备，它针对的是网络第3层数据包（例如IP数据包）。
使用TUN/TAP，我们可以创建虚拟网络接口。

用户空间程序通常连接到TUN/TAP虚拟网络接口。
操作系统通过TUN/TAP网络接口发送的数据包被传递到用户空间程序。
另一方面，程序通过TUN/TAP网络接口发送的数据包被注入到操作系统网络堆栈。
对于操作系统来说，数据包来自一个通过虚拟网络接口的外部源。

当程序连接到TUN/TAP接口时，内核发送到该接口的数据包将通过管道传送到程序中。
另一方面，通过程序写入接口的数据包将通过进入内核，
就好像它们是通过这个虚拟网络接口从外部来的一样。
该程序可以使用标准的{\tt read()}和{\tt write()}系统调用从虚拟接口接收数据包或将数据包发送到虚拟接口。

本任务的目的是熟悉TUN/TAP技术。
我们将通过几个任务来了解TUN/TAP接口的技术细节。
我们将使用以下Python程序作为实验的基础，并在整个实验过程中修改此代码。
该代码已包含在zip文件的\texttt{volumes} 文件夹中。

\begin{lstlisting}[caption={创建TUN接口 (\texttt{tun.py})}, label=vpn:list:create_tun]
#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# 创建一个tun接口
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tun%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# 获取接口的名称
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

while True:
   time.sleep(10)
\end{lstlisting}


% *******************************************
% SUBSECTION
% ******************************************* 
\subsection{Task 2.a: 接口名称}

我们将在主机\hostu 上运行\texttt{tun.py}程序。我们需要把上述\texttt{tun.py}程序设置成可执行，并使用root权限来运行它。请参阅以下命令：

\begin{lstlisting}
// 使程序可执行
# chmod a+x tun.py

// 使用root权限运行它
# tun.py
\end{lstlisting}

一旦程序执行，它将进入阻塞状态。你可以切换到另一个终端，并在容器中打开一个新的命令行界面。接着，打印出机器上所有的网络接口。请在执行以下命令后，报告你的观察结果：

\begin{lstlisting}
# ip address
\end{lstlisting}

你应该能够找到一个名为\texttt{tun0}的接口，你的任务是修改\texttt{tun.py}程序，并使用你的姓氏（拼音）作为其名称的前缀。例如，如果你姓张，则应使用\texttt{zhang}作为前缀。如果你的姓太长，可以使用前五个字符。请展示你的结果。

% *******************************************
% SUBSECTION
% ******************************************* 
\subsection{Task 2.b: 设置TUN接口}

此时，TUN接口是不可用的，因为它还没有被配置。
在使用接口之前，我们需要做两件事。
首先，我们需要为其分配一个IP地址。
其次，我们需要打开该接口，因为接口还处于关闭（down）状态。
我们可以使用以下两个命令进行配置：

\begin{lstlisting}
// 为接口分配IP地址
# ip addr add 192.168.53.99/24 dev tun0

// 打开该接口
# ip link set dev tun0 up
\end{lstlisting}

为了方便起见，同学们可以在\texttt{tun.py}中添加如下两行代码，
这样就可以由程序自动进行配置了。

\begin{lstlisting}
os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))
\end{lstlisting}

运行上面两个命令后，再次运行\texttt{"ip address"}命令，报告你的观察结果。
这与运行配置命令之前有何不同？

% *******************************************
% SUBSECTION
% ******************************************* 
\subsection{Task 2.c: 从TUN接口读取数据包}

在此任务中，我们将从TUN接口读取数据。
从TUN接口读出来的任何内容都是IP数据包。
我们可以将从接口接收到的数据转换成一个Scapy \texttt{IP}对象，
这样我们就可以打印出IP数据包的每个字段。
请使用以下\texttt{while}循环替换\texttt{tun.py}中的循环：

\begin{lstlisting}
while True:
  # 从tun接口获取一个数据包
  packet = os.read(tun, 2048)
  if packet:
    ip = IP(packet)
    print(ip.summary())
\end{lstlisting}

请在主机\hostu 上运行修改后的\texttt{tun.py}程序，并相应配置TUN接口，然后进行以下实验。
请描述一下你观察到的现象：

\begin{itemize}
  \item 在主机\hostu 上，\texttt{ping} 一下\texttt{192.168.53.0/24}网络中的主机。\texttt{tun.py}程序打印出了什么？发生了什么事？为什么？
  \item 在主机\hostu 上，\texttt{ping} 内部网络 \texttt{192.168.60.0/24} 中的主机，\texttt{tun.py} 是否打印出任何内容？为什么？
\end{itemize}

% *******************************************
% SUBSECTION
% ******************************************* 
\subsection{Task 2.d: 将数据包写入 TUN 接口}

在这个任务中，我们将把数据写入TUN接口。
由于这是一个虚拟网络接口，因此应用程序写入该接口的任何内容都将以IP数据包的形式出现在内核中。

我们将修改\texttt{tun.py}程序，以便从TUN接口得到一个数据包后
我们可以根据收到的数据包构造一个新的数据包。
然后我们将新数据包写入TUN接口。
如何构建新数据包取决于学生。
以下代码显示了如何将一个IP数据包写入TUN接口的示例。

\begin{lstlisting}
# 使用tun接口发送一个伪造的数据包。
newip = IP(src='1.2.3.4', dst=ip.src)
newpkt = newip/ip.payload
os.write(tun, bytes(newpkt))
\end{lstlisting}

请根据以下要求修改\texttt{tun.py}代码：

\begin{itemize}
  \item 从TUN接口得到一个数据包后，如果这个包是 ICMP echo request 数据包，
        则构造一个对应的 echo reply 数据包写入到TUN接口。
        请证明你的代码按预期工作了。
  \item 不要将IP数据包写入接口，而是将一些任意数据写入接口，并报告你的观察结果。
\end{itemize}


% *******************************************
% SECTION
% *******************************************
\section{Task 3: 通过隧道将IP数据包发送到VPN服务器}

在这个任务中，我们将从TUN接口接收到的IP数据包放入一个新的IP数据包的UDP载荷字段中，并将其发送到另一台计算机。也就是说，我们将原始数据包放在一个新的数据包里面。这被称为IP隧道技术。隧道的实现仅仅是标准的客户端/服务器编程。它可以建立在TCP或UDP之上。在这个任务中，我们将使用UDP。也就是说，我们将一个IP数据包放入UDP数据包的载荷字段中。


\paragraph{服务器程序\texttt{tun\_server.py}}
我们将在VPN Server上运行\texttt{tun\_server.py}程序。
这个程序只是一个标准的UDP服务器程序。
它监听端口\texttt{9090}并打印出接收到的任何内容。
程序假设UDP有效负载字段中的数据是一个IP数据包，
因此它将有效负载转换为Scapy \texttt{IP} 对象，并打印出这个IP数据包的源IP地址和目标IP地址。

\begin{lstlisting}[caption={\texttt{tun\_server.py}}, label=vpn:list:tun_server]
#!/usr/bin/env python3

from scapy.all import *

IP_A = "0.0.0.0"
PORT = 9090

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))

while True:
   data, (ip, port) = sock.recvfrom(2048)
   print("{}:{} --> {}:{}".format(ip, port, IP_A, PORT))
   pkt = IP(data)
   print("   Inside: {} --> {}".format(pkt.src, pkt.dst))
\end{lstlisting}


\paragraph{实现客户端程序\texttt{tun\_client.py}}
首先我们需要修改TUN程序 \texttt{tun.py}。把它重命名为\texttt{tun\_client.py}。
使用UDP向另一台计算机发送数据可以使用标准套接字（socket）编程。

将程序中的\texttt{while}循环中的\texttt{SERVER\_IP} 和 \texttt{SERVER\_PORT} 替换为VPN Server上运行的服务器程序的实际IP地址和端口号。

\begin{lstlisting}
# Create UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
   # 从tun接口获取一个数据包
   packet = os.read(tun, 2048)
   if packet:
      # 通过隧道传输一个数据包
      sock.sendto(packet, (SERVER_IP, SERVER_PORT))
\end{lstlisting}

\paragraph{测试}
在VPN Server上运行\texttt{tun\_server.py}程序，
然后在主机\hostu 上运行\texttt{tun\_client.py}。
为了测试隧道是否工作，请\texttt{ping}任何一个属于\texttt{192.168.53.0/24}网络的IP地址。
VPN Server上打印的内容是什么？为什么？

我们的最终目标是使用隧道访问私有网络\texttt{192.168.60.0/24}内的主机。
让我们\texttt{ping}主机\hostv，看看ICMP数据包是否通过隧道发送到了VPN Server。
如果没有，问题出在哪？
你需要解决这个问题，这样才能通过隧道发送\texttt{ping}数据包。
这是通过路由完成的，去往\texttt{192.168.60.0/24}网络的数据包应该被路由到TUN接口并被交给\texttt{tun\_client.py}
程序。以下命令显示如何在路由表中添加新的条目。

\begin{lstlisting}
# ip route add <network> dev <interface> via <router ip>
\end{lstlisting}

请证明当你 \texttt{ping}一个在\texttt{192.168.60.0/24}网络中的IP地址时，ICMP数据包
是通过隧道到达的。


% *******************************************
% SECTION
% *******************************************
\section{Task 4: 设置VPN服务器}

在\texttt{tun\_server.py}从隧道中获取数据包后，它需要将数据包提供给内核，
以便内核可以将数据包路由到其最终目的地。
这需要通过一个TUN接口来完成，就像我们在Task 2中所做的一样。
请修改\texttt{tun\_server.py}，使其可以执行以下操作：

\begin{itemize}
  \item 创建一个TUN接口并对其进行配置。
  \item 从 socket 接口获取数据，将接收到的数据视为IP数据包。
  \item 将数据包写入 TUN 接口。
\end{itemize}

在运行修改后的\texttt{tun\_server.py}之前，我们需要打开IP转发（IP forwarding）。
除非有特别配置，否则计算机将仅充当主机，而不充当网关。
VPN服务器需要在私网和隧道之间转发数据包，所以需要起到网关的作用。
我们需要为计算机启用IP转发，使其行使网关的功能。
我们在路由器容器上已经开启了IP转发。
你可以在\texttt{docker-compose.yml}中看到路由器容器的以下条目：

\begin{lstlisting}
sysctls:
        - net.ipv4.ip_forward=1
\end{lstlisting}

\paragraph{测试} 如果一切设置正确，我们可以从主机\hostu \texttt{ping}主机\hostv 。
ICMP回显请求数据包最终应该通过隧道到达主机 \hostv。
请展示你的验证结果。
需要注意的是，虽然主机 \hostv 会响应ICMP数据包，
但reply不会回到主机\hostu ，因为我们还没有设置好一切。
因此，对于此任务，显示（使用Wireshark或tcpdump）ICMP数据包已到达主机\hostv 就足够了。

% *******************************************
% SECTION
% *******************************************
\section{Task 5: 处理双向流量}

到这里，我们的隧道的一个方向就完成了，即我们可以通过隧道将数据包从主机\hostu 发送到主机\hostv 。
如果我们查看主机\hostv 上的Wireshark，可以看到主机\hostv 已发出响应，但数据包在某处被丢弃。
这是因为我们的隧道只设置好了一个方向，我们还需要设置它的另一个方向，返回的流量才能通过隧道返回到主机\hostu。

为了设置好另外一个方向，我们的TUN客户端和服务器程序需要从两个接口读取数据，
即TUN接口和 socket 接口。
所有这些接口都由文件描述符（file descriptor）表示，
所以我们需要监视它们以查看是否有来自它们的数据。
一种方法是不断去查询它们，看看每个接口上是否有数据。
这种方法的性能是不可取的，因为当没有数据时，进程必须仍然做着空循环。
另一种方法是轮流从接口上读取数据。在默认情况下，读是阻塞的，即如果没有数据，进程将等待在那。
当有数据可用时，进程才能继续执行。这样就不会在没有数据的时候浪费CPU时间。

基于读取的阻塞机制适用于一个接口。
如果一个进程需要从多个接口上读取数据，它不能只在其中一个接口上阻塞，否则别的接口有数据时，
进程没法去处理。进程必须能监控所有接口。
\linux 有一个称为\texttt{select()}的系统调用，它允许程序同时监视多个文件描述符。
为了使用\texttt{select()}，进程需要将所有要监视的文件描述符存储在一个数组中，
然后将它给\texttt{select()}系统调用。系统会阻塞该进程，直到其中的一个文件描述符上有数据可用。
我们可以检查哪个文件描述符接收到了数据。
在下面的Python代码片段中，我们使用\texttt{select()}来监控\texttt{TUN}和 socket 文件描述符。

\begin{lstlisting}
# 我们假设 sock 和 tun 文件描述符已经被创建。

while True:
  # 程序将被阻塞，直到其中至少有一个接口上有数据
  ready, _, _ = select.select([sock, tun], [], [])

  for fd in ready:
    if fd is sock:
       data, (ip, port) = sock.recvfrom(2048)
       pkt = IP(data)
       print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
       ... (code needs to be added by students) ...

    if fd is tun:
       packet = os.read(tun, 2048)
       pkt = IP(packet)
       print("From tun    ==>: {} --> {}".format(pkt.src, pkt.dst))
       ... (code needs to be added by students) ...
\end{lstlisting}

同学们可以用上面的代码替换TUN客户端和服务端程序中的\texttt{while}循环。代码是不完整的，缺的部分留给同学们自己完成。

\paragraph{测试。} 完成上面的操作后，我们应该能够在主机 \hostu 与 \hostv 之间通信，VPN 隧道（未加密）就算完成了。
请使用 \texttt{ping} 和 \texttt{telnet}命令来验证，并用wireshark提供证明。在你的报告中，需要指出你的数据包是如何流动的。


% *******************************************
% SECTION
% *******************************************
\section{Task 6: 隧道中断实验}

在主机\hostu 上远程登录到主机\hostv 。
在保持\texttt{telnet}连接的同时，
我们通过中断\texttt{tun\_client.py}或\texttt{tun\_server.py}程序来中断VPN隧道。
然后我们在\texttt{telnet}窗口中输入一些内容。
你看到输入的内容了吗？TCP连接会发生什么？连接是否断开？

现在让我们重新连接VPN隧道（不要等待太久）。
我们再次运行\texttt{tun\_client.py}和\texttt{tun\_server.py}程序，
并设置它们的TUN接口和路由。一旦隧道重新建立，\texttt{telnet}连接会发生什么？
请描述并解释你的观察结果。


% *******************************************
% SECTION
% *******************************************
\section{Task 7: 主机\hostv 上的路由实验}

在真实的VPN系统中，流量会被加密（本实验不涉及这部分）。
这意味着返回流量必须从同一隧道返回。
如何获取从主机\hostv 到VPN服务器的返回流量并非易事。
我们的设置简化了这种情况。
在我们的设置中，主机\hostv 的路由表有一个默认设置：
去往任何目的地的数据包，除了\texttt{192.168.60.0/24}网络，将被自动路由到VPN服务器。

在现实世界中，主机\hostv 可能距离VPN服务器有多跳，
默认路由条目可能无法保证将返回的数据包路由回VPN服务器。
我们必须正确设置专用网络内的路由表，以确保将去往隧道另一端的数据包路由到VPN服务器。
为了模拟这种情况，我们从主机\hostv 中删除默认条目，
然后在路由表中添加一个更具体的条目，以便可以将返回的数据包路由回VPN服务器。
学生可以使用以下命令删除默认条目并添加新条目：

\begin{lstlisting}
// 删除默认条目
# ip route del default

// 添加一个条目
# ip route add <network prefix> via <router ip>
\end{lstlisting}


% *******************************************
% SECTION
% *******************************************
\section{Task 8: 专用网络之间的VPN}

\begin{figure}[htb]
  \begin{center}
    \includegraphics[width=0.8\textwidth]{Figs/VPN_3lans.pdf}
  \end{center}
  \caption{两个专用网络之间的VPN}
  \label{vpn:fig:two-private-network}
\end{figure}

在此任务中，我们将在两个专用网络之间建立VPN。
设置如图~\ref{vpn:fig:two-private-network}所示。
整个设置放在了\texttt{docker-compose2.yml}文件中，
我们可以使用\texttt{"-f docker-compose2.yml"}选项让\texttt{docker-compose}使用该文件，
否则默认的\texttt{docker-compose.yml}文件会被使用。

\begin{lstlisting}
$ docker-compose -f docker-compose2.yml build
$ docker-compose -f docker-compose2.yml up
$ docker-compose -f docker-compose2.yml down
\end{lstlisting}

此设置模拟了一个机构有两个站点的情况，每个站点都有一个专用网络。
连接这两个网络的唯一方法是通过互联网。
你的任务是在这两个站点之间建立VPN，因此这两个网络之间的通信将通过VPN隧道。
你可以使用之前开发的代码，但你需要考虑如何设置正确的路由，
以便这两个专用网络之间的数据包可以路由到VPN隧道。
在你的报告中，请描述并解释你的工作。
你需要提供证据证明这两个专用网络之间的数据包确实通过了VPN隧道。


% *******************************************
% SECTION
% *******************************************
\section{Task 9: 用TAP接口实验}

在这个任务中，我们将用TAP接口做一个简单的实验，让学生对这种类型的接口有所了解。
TAP接口的工作方式与TUN接口非常相似。
主要区别在于TUN接口的内核端挂接到IP层，而TAP接口的内核端挂接到MAC层。
因此，通过TAP接口的数据包包含MAC头，而通过TUN接口的数据包只包含IP头。
使用TAP接口，应用程序除了获取包含IP数据包的帧外，还可以获取其他类型的帧，例如ARP帧。

我们将使用以下程序进行实验，我们将只使用VPN客户端容器（两个实验的环境设置任何一个都可以）。
创建TUN接口和TAP接口的代码是比较相似的，唯一的区别在于接口类型。
对于TAP接口，我们使用\texttt{IFF\_TAP}，而对于TUN，我们使用\texttt{IFF\_TUN}。
其余代码相同，以下不再赘述。
TAP接口的配置方法与TUN接口的配置方法完全相同。

\begin{lstlisting}
...

tap = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tap%d', (*@\textbf{IFF\_TAP}@*) | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tap, TUNSETIFF, ifr)
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
... 

while True:
   packet = os.read(tap, 2048)
   if packet:
      ether = Ether(packet)
      print(ether.summary())
\end{lstlisting}

上面的代码只是从TAP接口读取。
然后它将数据转换为Scapy \texttt{Ether} 对象，并打印出其所有字段。
请 \texttt{ping} 一下 \texttt{192.168.53.0/24} 网络中的一个IP地址，
在报告中描述并解释你观察到的现象。

我们可以做一件有趣的事，一旦你从TAP接口得到一个以太网帧，你可以检查它是否是一个ARP请求，
如果是，则生成相应的ARP回复并将其写入TAP接口。
下面提供了一个示例代码：

\begin{lstlisting}
while True:
   packet = os.read(tun, 2048)
   if packet:
      print("--------------------------------")
      ether = Ether(packet)
      print(ether.summary())

      # 发送一个伪造的ARP请求
      FAKE_MAC   = "aa:bb:cc:dd:ee:ff"
      if ARP in ether and ether[ARP].op == 1 :
         arp       = ether[ARP]
         newether  = Ether(dst=ether.src, src=FAKE_MAC)
         newarp    = ARP(psrc=arp.pdst, hwsrc=FAKE_MAC,
                         pdst=arp.psrc, hwdst=ether.src, op=2)
         newpkt     = newether/newarp

         print("***** Fake response: {}".format(newpkt.summary()))
         os.write(tun, bytes(newpkt))
\end{lstlisting}

为了测试你的TAP程序，你可以对任何IP地址运行\texttt{arping}命令。
该命令通过指定的接口向指定的IP地址发出ARP请求。
如果你伪造arp回复的程序工作正常，你应该能够得到响应。
请参阅以下示例。

\begin{lstlisting}
arping -I tap0 192.168.53.33
arping -I tap0 1.2.3.4
\end{lstlisting}


% *******************************************
% SECTION
% ******************************************* 
\section{提交}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/submission}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


\end{document}



